feat!(ethexe): malachite#5397
Draft
grishasobol wants to merge 74 commits intomasterfrom
Draft
Conversation
Intermediate state before switching the producer to pull quarantine status directly from the Database. This commit is about to be superseded.
…atabase
Replace the rolling eth_head_history in State with a direct read of
the current EB chain head from DBGlobals::latest_synced_block. New
quarantine module exposes two helpers built on top of ethexe-db:
- anchor(db, q): producer picks the youngest EB that has ≥ q
canonical descendants, matching ethexe-compute's
find_canonical_events_post_quarantine semantics.
- verify_passed(db, candidate, q): validators reject a proposal
whose AdvanceTillEthereumBlock hash isn't an ancestor of the
local head at depth ≥ q. Genesis is accepted unconditionally so
the short-chain fallback stays consistent between the two sides.
State::validate_proposal_parts now enforces exactly one
AdvanceTillEthereumBlock tx and runs it through verify_passed; the
proposer path (app::GetValue) calls State::quarantine_anchor and
falls back to the genesis hash when the DB walk fails (e.g. we
haven't synced enough blocks yet).
The chain_head_tx/rx mpsc is gone along with
MalachiteService::receive_new_chain_head and the call site in
ethexe-service's event loop — the producer reads DB state directly
at GetValue time, which is also what gives validators a definition
of "the local view" they can compare a proposal against.
MalachiteConfig renames quarantine_depth: u32 to
canonical_quarantine: u8 so the same value flows end-to-end between
Malachite and ComputeConfig; default is
ethexe_common::gear::CANONICAL_QUARANTINE.
MalachiteService::new now takes Database; ethexe-service passes
db.clone() on the live path and on the test harness. No changes to
block/transaction shape — this commit is strictly about how the
anchor is chosen and verified.
Switch the producer and validators from DBGlobals::latest_synced_block to the latest SimpleBlockData received via the observer event stream. The block-header walk still reads ethexe-db, but the reference point is now `State::latest_received_head: Option<SimpleBlockData>`, overwritten on every MalachiteService::receive_new_chain_head call. A dedicated mpsc carries the chain-head updates into the app task; no history is retained — only the most recent value. `latest_synced_block` trails the event stream because it only updates after extra sync processing, so it was producing stale anchors. `ethexe-service`'s event loop now passes the `Observer::Block` payload to both `consensus` and `malachite`. quarantine::anchor now returns `Option<H256>`: `None` when the local chain is still within `canonical_quarantine` of genesis. On that signal the producer simply omits the `AdvanceTillEthereumBlock` tx from the MB — no more genesis fallback. validate_proposal_parts tolerates zero AdvanceTillEthereumBlock txs (legal producer choice), rejects two+, and for one runs the verify against the local latest head (failing when no head has been received yet). quarantine::verify_passed lost its genesis-is-always-ok special case, which was only needed to accommodate the fallback we just removed.
…ash dedup
InjectedTxMempool now knows about reference_block mortality, matching
the rules ethexe-consensus already enforces (tx_validation.rs):
- insert rejects a tx when
* its hash is in the seen-hash table (already committed within
VALIDITY_WINDOW), or
* its reference_block is not yet in the DB, or
* reference_block.height + VALIDITY_WINDOW ≤ latest_head.height,
or
* the pool is at DEFAULT_POOL_CAPACITY (10_000).
- set_chain_head(head) is the single GC trigger: it overwrites the
tracked head height and purges both the pool and the seen map of
entries whose reference_block has aged out.
- fetch(head, _gas_budget) is now non-destructive. It returns only
txs whose reference_block is a canonical ancestor of `head` within
VALIDITY_WINDOW steps; everything else stays put, so a reorg that
flips a branch back in makes the tx eligible again without loss.
- forget(committed) moves the given txs out of the pool and records
their hashes in the seen map under their reference_block, so a
re-gossipped duplicate cannot slip back in before aging out.
Malachite builds only on top of finalized blocks, so
finalize → forget is sufficient for dedup; there is no round-local
state to unwind.
Mempool trait gets the new set_chain_head + head-aware fetch.
EmptyMempool and the app task are updated accordingly. The app now
also forwards observer-delivered chain heads into the mempool and,
on AppMsg::Finalized, extracts the Injected(..) variants out of the
committed SequencerBlock and hands them to forget — for that
State::commit now returns the committed block.
Variant A of the validator-identity unification. All of the changes
are local to ethexe-malachite + ethexe-service; no upstream malachite
crate is forked or patched.
context.rs
- type SigningScheme = K256 (from malachitebft-signing-ecdsa, using
the RustCrypto k256 curve backend).
- Address becomes a newtype over gsigner::secp256k1::Address;
from_public_key does keccak256(uncompressed_pubkey[1..])[12..] —
same derivation the rest of ethexe uses on-chain.
- PublicKey / Signature / PrivateKey are the corresponding
malachitebft-signing-ecdsa wrappers around k256 types.
- Validator / ValidatorSet / Vote / Proposal / ProposalPart keep
their shape, minus the ed25519-specific Address::from_public_key
helper. Validator gains with_address(…) so genesis entries can be
loaded without recomputing the address.
- EthexeSigner is now an ECDSA signer backed by a PrivateKey<K256>;
signs/verifies votes, proposals, extensions. The same 32-byte
secret will later back libp2p and on-chain signing too.
genesis.rs (new)
- MalachiteGenesis { validators: Vec<GenesisValidator> } loaded
from home_dir/genesis.json.
- Each entry is consistency-checked: declared address must equal
the one derived from the declared public key. Mismatches error
out early.
- to_validator_set() materializes a sorted, deterministic
ValidatorSet.
lib.rs
- MalachiteService::new now takes (signer: gsigner::Signer<Secp256k1>,
validator_pub_key) — the key is the ethexe validator key. The
32-byte secret is exported once from the keyring and drives:
* Malachite votes/proposals (via EthexeSigner),
* libp2p identity (Keypair built from
libp2p_identity::secp256k1::SecretKey::try_from_bytes),
* on-chain commitments (via the shared gsigner::Signer).
So a node presents a single identity across all three layers.
- node_key.json path / load_or_generate_node_key are gone; peer id
is now deterministic from the validator key.
- ValidatorSet sourced from genesis.json at init; the service
checks that the local validator appears in the set and fails
loudly otherwise.
ethexe-service
- malachite: Option<MalachiteService> — only built when the node
has a validator key. Non-validator nodes skip Malachite entirely;
the event loop uses maybe_next_some() and the receive_* calls
are gated behind if let Some(..).
- new() plumbs signer.clone() + validator_pub_key into the
MalachiteService; test harness keeps malachite = None (tests
don't exercise consensus yet).
codec.rs
- drops the ed25519_consensus::Signature import, uses
context::Signature; SignedMessage raw form carries the wrapped
ECDSA signature directly (no .inner() unwrap to k256 types).
Cargo
- workspace: add malachitebft-signing-ecdsa with features
["k256","rand","serde","std"].
- ethexe-malachite: replace malachitebft-signing-ed25519 with
malachitebft-signing-ecdsa, add k256 and libp2p-identity (for
building the secp256k1 libp2p keypair), add gsigner.
…ool insert Self-audit fallout: - quarantine::anchor / quarantine::verify_passed now take start_block_hash (from DBGlobals::start_block_hash) instead of genesis_block_hash. Walks cannot cross the oldest block the local DB is guaranteed to have; crossing it would read a parent header that isn't stored. anchor returns Ok(None) when the walk would need to go past start_block before finishing canonical_quarantine steps; verify_passed returns Err, so the validator simply skips voting — that's an acceptable outcome per the design. - mempool::recent_ancestors walks until start_block (previously: until H256::zero or a cycle). Fixes the same bug on the mempool side — a ref_block older than start_block would previously pass the ancestry test via an unbounded walk that relied on DB returning None to stop. - mempool::insert now requires the ref_block to resolve to a header unconditionally. Previously we only checked when a head had been observed, which let stale txs sit in the pool on a fresh node until the first head arrived. Rejecting outright is safer; the sender can re-gossip after our DB catches up. - mempool::is_expired uses saturating_add, guarding against u32 overflow on pathological inputs. - State::genesis_block_hash is gone (it was only used for the anchor fallback in the producer path, which we already removed when quarantine::anchor started returning Option). Producer now just skips AdvanceTillEthereumBlock when anchor says None. No behaviour change for full-sync nodes where start_block == genesis.
…t-paced producer
Separate the Malachite libp2p peer_id from the ethexe-network swarm by
domain-separated keccak256 derivation from the validator secret —
operators still manage one master key, but the two swarms no longer
share a peer_id (cleaner observability, no cross-protocol routing
ambiguity). The validator key still signs Malachite votes/proofs, so
peers tie libp2p identity to the on-chain validator via the existing
`sign_validator_proof` flow.
Wire `--malachite-persistent-peer` through CLI / `MalachiteCliConfig` /
`MalachiteConfig` / Malachite's `P2pConfig::persistent_peers` so
multi-node deployments can be brought up without the (still disabled)
discovery layer. New `ethexe malachite peer-id <pubkey>` subcommand
derives the libp2p peer_id offline so operators can populate
multiaddrs without having to boot a node first.
Producer pacing rework:
- `LinearTimeouts.propose = SLOT_DURATION + 1s`. Non-proposer
tolerates one ETH slot of silence before incrementing the round.
- On `GetValue` cache miss, the proposer evaluates a four-way
decision tree based on the parent MB's `last_advanced_block`:
* candidate quarantine-passed EB is a strict descendant ⇒
advance + propose immediately;
* candidate equals or is unreachable from the parent's anchor
(rare deep reorg) ⇒ log::error + skip the advance for this
MB;
* no advance but mempool has txs ⇒ propose with txs;
* nothing to propose ⇒ wait until either a chain-head event
or `Mempool::wait_for_new_tx` fires (no deadline — ETH
delivers a fresh slot every ~12s in normal operation).
- `last_advanced_block` is propagated forward on every BlockProposal
by the service handler: latest `AdvanceTillEthereumBlock` in the
MB's transactions wins, otherwise the parent MB's value is
inherited (zero for the genesis MB).
- `is_strict_descendant_of` quarantine helper + unit tests.
- `Mempool::wait_for_new_tx` (Notify-backed in `InjectedTxMempool`,
pending-forever in `EmptyMempool`).
- `MbMeta` gains `last_advanced_block: H256`.
Finalization is intentionally not paced: `target_time` stays `None`
in `HeightParams`, so a successful commit hits the application
immediately. The slot-based pacing applies only to the propose phase.
…, SequencerBlock hash
Backfill unit tests for pieces that landed in earlier commits without
coverage:
- InjectedTxMempool — 9 cases covering insert/fetch/forget/wakeup
contracts (unknown ref-block rejection, hash dedup, capacity cap,
set_chain_head purge, canonical-ancestor filter, Notify-based
`wait_for_new_tx` on success / non-wakeup on rejected insert).
- MalachiteGenesis::load — 6 cases covering missing-file, empty
set, address/pubkey-mismatch rejection, voting-power default,
consistent-load happy path, and `to_validator_set` count.
- libp2p key derivation — `derive_libp2p_secret` is deterministic
and distinct from the validator secret it was derived from;
`malachite_libp2p_peer_id` is a pure function of the validator
secret (operators rely on offline derivation).
- SequencerBlock — hash is content-addressed (changes with parent
or transactions), `Transaction::tag()` mapping is pinned, SCALE
round-trip preserves the hash.
Adds `tempfile` to ethexe-malachite dev-dependencies for genesis
file-load tests. No production-code changes — the few logic touches
are in test-only scope.
…rticipant Reshapes ethexe-consensus around malachite-finalized sequencer blocks (MBs): - ChainCommitment.head is now an MB hash (H256), not announce hash. - BatchCommitmentValidationRequest.head: Option<H256>. - BlockMeta.last_committed_announce → last_committed_mb. - Solidity event AnnouncesCommitted → ChainCommitted; ABI artifacts refreshed. - Validator state machine reduced to WaitForEthBlock / Coordinator / Participant. Producer + Subordinate + announce sync are gone. - Coordinator aggregates outcomes from finalized MBs walking mb_meta.parent_mb_hash and submits the existing BatchCommitment shape to Router unchanged. - Participant accepts request.head if it equals or is an ancestor of latest_finalized_mb, otherwise drops the signature with a warning. - Coordinator-side aggregation has a configurable delay (CLI flag --coordinator-aggregation-delay-ms, default 1500ms) so participants can catch up on the same chain head and the previous MB has time to finish executing. - Empty MB outcomes never produce a chain commitment on their own; batches without chain/codes/validators/rewards are skipped. - ConnectService is gone — non-validator nodes run with consensus = None. - timelines.block_producer_at → timelines.block_coordinator_at. DB migrations are not preserved (POC); fast_sync is parked behind a no-op until the MB-driven recovery path lands. Service- and batch-level tests are stripped and will be reintroduced in the next commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Bump EXPECTED_TYPE_INFO_HASH for BlockMeta + DBGlobals shape changes (db.rs, migrations/v3.rs). - ethexe-rpc: rename `calculate_next_producer` → `next_coordinator` in the test module to follow the production rename. - ethexe-service: thread the new `coordinator_aggregation_delay` knob through `NodeConfig` smoke test, drop the `chain_deepness_threshold` field, switch `ConnectService` users to `consensus = None`, and rename `block_producer_index_at` → `block_coordinator_index_at`. - The `tests/mod.rs` integration scenarios (~6k lines, all built on the announce harness that no longer exists) are wrapped in a `#[cfg(any())]` module so they keep parsing. The `utils` sub-module stays compiled because the lib references `tests::utils::TestingEvent`. The cases will be rebuilt against the MB-driven flow in a follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rebuilds the batch round-trip test suite that was deleted along with the announce-driven mocks. New cases cover the same surface as before but are wired against MB chains seeded directly into the database: - accepts_matching_request — create→validate happy path. - rejects_duplicate_code_ids - rejects_unknown_code_in_request - rejects_code_not_processed_yet - rejects_digest_mismatch - rejects_head_mb_not_in_chain — replaces the old "non-best announce" case; the manager rejects when request.head is foreign to the chain. - rejects_head_mb_not_computed — head MB exists but is not yet finalized in the local state. - rejects_empty_batch_request — synthetic empty request fails the "empty batch" gate. - batch_size_limit_exceeded_is_rejected_on_validation - squash_orders_negative_value_transitions_first — sender-first sort preserved end-to-end through the squash and the validation digest matches. Helpers `append_mb`, `setup_mb_chain`, `prepare_canonical_batch`, and `mock_batch_manager` ride on the existing `BlockChain::mock` Eth-side scaffolding, plus a `MockElectionProvider` from `ethexe-ethereum` so the manager's middleware dependency is satisfied even though the covered cases never trigger validators-commitment aggregation. Drops the now-unused `BatchCommitmentManager::replace_limits` helper since each test uses its own manager instance. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds back the validators-commitment cases that were dropped along with the announce mocks. The new test threads a `MockElectionProvider` handle through `mock_batch_manager_with_limits_and_election`, sets up canned election results at the right era boundaries, and walks the manager through: - block before election start → no commitment - block right at election start for era 1 → commits validators1, era 1 - block deeper in era 1 election period → same commitment - same block after marking era 1 already committed → no commitment - block at era 2 election start with only era 0 committed → still commits validators2 for era 2 (warning logged) - block tagged as having era 3 already committed → errors out (committing past the next era is a protocol invariant violation) Also nudges the chain config to a 100s era / 50s election so block indices land on the era boundaries we want. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…sses Brings the integration ping test back to life under the MB-driven flow. Three changes were needed: 1. ethexe-malachite: expose `write_test_genesis(path, signer, pub_keys)` so tests can derive a malachite genesis JSON straight from a gsigner keystore without going through the production CLI/keygen flow. 2. ethexe-service tests: each `Node::start_service` now boots a real single-node `MalachiteService` (binding to 127.0.0.1:0 so parallel tests don't fight over ports), threads a `MockElectionProvider`- backed coordinator through, and hands the service a tempdir as malachite home. `Service::new_from_parts` learned to take an `Option<MalachiteService>` + gas allowance so connect-mode nodes keep their `None`. The `ping` test moved out of the disabled `#[cfg(any())]` block. `WaitForProgramCreation` and `WaitForReplyTo` now share the same force-mine hack `WaitForUploadCode` already had — without periodic `evm_mine` calls Anvil sits idle after the last user tx and the coordinator never gets a fresh ETH head to commit the program reply. 3. Producer: `AdvanceTillEthereumBlock` was emitted as a single tx pointing at the youngest descendant, so events from intermediate blocks (program creations, mirror messages, etc.) silently dropped on the floor. The new `collect_advance_chain` walks from the parent MB's `last_advanced_block` to the candidate and the producer emits one `AdvanceTillEthereumBlock` per block in the gap, capped at 1024 to bound catch-up bursts. ethexe-service eagerly persists the chain-head's header on `ObserverEvent::Block` so the producer's `is_strict_descendant_of` check doesn't race the observer's sync. `cargo nextest run -p ethexe-*`: 327 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single \`AdvanceTillEthereumBlock { eth_block_hash }\` tx is supposed to
process events for every Ethereum block from the parent MB's
\`last_advanced_block\` (exclusive) up to and including the target —
not just the target block alone. The previous wiring (one
AdvanceTillEthereumBlock tx per intermediate ETH block, emitted by
the producer) was the wrong fix and silently dropped events when the
producer-side walk was bypassed.
This commit moves the range walk into the processor:
- \`Processor::process_transitions\` takes a new
\`initial_advanced_block\` argument and tracks a per-MB
\`current_anchor\`. Each AdvanceTillEthereumBlock walks the
canonical chain (\`parent_hash\`) from \`current_anchor\` to the tx's
target, processes events for every block in that range, and bumps
the anchor.
- \`Processor::collect_advance_chain\` performs the walk; the safety
cap is 1024 hops, and a missing parent header partway through the
walk is treated as a graceful fence (DB doesn't reach back that
far) so the genesis MB still produces transitions when the local
chain doesn't extend to genesis-zero.
- Two new \`ProcessorError\` variants surface "target header missing"
and "walk exceeded cap".
- \`mb_compute\` reads parent MB's \`last_advanced_block\` from
\`MbMeta\` and passes it through.
- The \`ProcessorExt\` trait + the test mock in \`ethexe-compute\` and
the smoke test in \`ethexe-processor\` are updated for the new
parameter.
Producer-side change is reverted: producer emits one
\`AdvanceTillEthereumBlock\` per MB pointing at the youngest descendant
the quarantine anchor allows, exactly as before this saga started.
\`cargo nextest run -p ethexe-*\`: 327 passed, 1 skipped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n commit
Plug two structural gaps that surfaced once the multi-validator test
went from N=3 to N=4 (quorum 3-of-4 lets BFT progress without one of
the validators, so value-sync actually kicks in):
1. `MalachiteEvent::{BlockProposal, BlockFinalized}` were emitted only
on the live path (proposer + completed-stream-at-current-height).
Synced and buffered-then-promoted MBs slipped through silently —
compute never ran, mb_meta.parent_mb_hash chains had holes, and
coordinator-side batch commitment then crashed with "MB chain walk
reached genesis". Move the DB writes (`set_mb_block`,
`mutate_mb_meta`, `globals_mutate(latest_finalized_mb_hash)`) into
the malachite app and gate every event behind a new `synced` flag
on `MbMeta`: a block is `synced` only when the `parent_mb_hash`
chain back to the genesis MB is fully recorded. Buffered events
drain once the chain closes, including a cascade through
`pending_by_parent` for out-of-order arrivals. Submit also
triggers from `StartedRound`'s pending-parts promotion, the path
that was previously silent.
2. The producer's `try_include_chain_commitment` propagated errors
from the strict backward walk, so any compute lag past the
on-chain commit anchor (or a fresh restart with an empty
malachite store) crashed the coordinator. Add
`collect_computed_uncommitted_predecessors` — walks the canonical
chain back from `mb_head`, returns the longest contiguous
*computed* prefix anchored at `last_committed_mb`, falls back to
an empty result instead of erroring. Producer commits whatever it
has; the rest accumulates for the next batch attempt. Participant
keeps the strict variant so an unverifiable request still rejects
the signature.
Also raise `MalachiteConfig::DEFAULT_GAS_ALLOWANCE` to
`DEFAULT_BLOCK_GAS_LIMIT` (4T) — 1B was four orders of magnitude too
small for `demo-async`'s round-trips. And add `Drop for
MalachiteService` that kills the engine actor and aborts the spawned
tasks so a stopped validator's libp2p / consensus tree doesn't keep
voting.
Test harness: per-validator moniker so logs are distinguishable, and
two new integration tests — `multiple_validators_ping` (3-of-3 smoke)
and `multiple_validators` (4-of-4 with stop/restart, exercises the
new synced and lenient-commit paths end-to-end).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolved conflicts by keeping our Announce-removal branch; the master changes that re-introduced Announce types in mock.rs, validator/topic.rs, and service/lib.rs are obsolete and discarded. Renamed the on-chain ChainCommitted event to AnnouncesCommitted to match the master contract; it's a label change only — semantics stays "MB head committed". Pulled in master's proptest helpers (scheduled_task_strategy, schedule_strategy, Arbitrary for MessageType / StateHashWithQueueSize) so the new ethexe-runtime-common::proptest module compiles. Bumped EXPECTED_TYPE_INFO_HASH after the new Arbitrary impls touched the type registry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…r pre-aggregation delay * `ethexe/service/src/lib.rs`: forward `config.node.canonical_quarantine` into `MalachiteConfig` so the producer's `AdvanceTillEthereumBlock` proposals match the depth that participants enforce — otherwise the producer proposes the chain head while validators reject as "needs ≥ default quarantine" and BFT deadlocks. * `ethexe/cli/src/params/node.rs`: default `coordinator_aggregation_delay_ms` to 0. With the MB-driven flow the coordinator no longer has to wait for compute to catch up to a specific Ethereum block (compute keys off `latest_finalized_mb_hash` inside BFT). On anvil's 2 s block time, any non-zero delay caused `CoordinatorBoot`'s pending future to be reset by the next chain head before it could submit, so no batch commitments ever fired in 3-validator local runs. Operators can still tune the value up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`MbMeta` gained a `finalized: bool` field in 458bfca. The new field goes inside the SCALE encoding so old records (34 bytes) cannot be decoded as the new layout (35 bytes). Existing on-disk databases must be wiped and re-initialised — bump the version constant so the explicit error fires instead of silent decode corruption at the next read. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…of MbMeta field Earlier in 458bfca / 02167df I added an `MbMeta::finalized` flag and bumped LATEST_VERSION to 6 to expose finalization as O(1) state. The flag is correct in code, but the SCALE schema break forces a wipe of every validator's on-disk state — and on a deployed cluster without a coordinated wipe across all nodes (one of which I do not operate), wipe-and-resync also resets the validator's view of the chain history that the Router contract still references via `last_committed_mb`. So the schema bump was the wrong primitive for the same correctness goal. Revert those two commits' on-disk shape: - `MbMeta`: drop `finalized`, restore the original three fields and the type-info hash. - `LATEST_VERSION`: back to 5. - `mark_block_as_finalized` no longer mutates `mb_meta`. Same strict semantics as 458bfca, just implemented as a one-pass walk instead of an indexed flag. New `utils::is_finalized_locally` walks back from `globals().latest_finalized_mb_hash` via `mb_compact_block.parent` and returns `true` iff `candidate` is reachable. Sound by BFT-safety: any two BFT-decided MBs are linearly ordered, so reachability through the parent pointer is iff for "finalized locally". Walk depth is bounded by the height gap between `latest_finalized_mb` and `head_mb` — single-digit in steady state (`coordinator_aggregation_delay / mb_block_time`). Behaviorally identical to the flag-based version: - Coordinator's `head_mb` finalized locally → walk finds it → accept. - Participant's finalization cascade lags behind coordinator's `head_mb` (cross-AS gossip, late vote propagation) → walk doesn't reach it → reject. Coordinator's next attempt picks up this participant once its cascade catches up. Speculative `BlockProposal` paths can still produce computed-but-not- finalized MBs in the local DB; the walk does not consider them, so chain commitments cannot reflect speculative-and-later-discarded blocks — same correctness gain as the flag. `HeadMbNotFinalized` and `HeadMbAlreadyCommitted` rejection reasons keep the new naming. `is_ancestor_or_equal` (and its tests) stay removed. New tests: - `is_finalized_zero_candidate_is_universally_finalized` - `is_finalized_self_is_finalized` - `is_finalized_resolves_proper_ancestor_of_finalized_head` - `is_finalized_returns_false_for_descendant_of_finalized_head` - `is_finalized_returns_false_when_no_local_finalization` - `is_finalized_returns_false_on_disjoint_chain` Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Strip transitional 'legacy', 'announce-driven', 'previous app.rs', 'NON_PROPOSER_PROPOSE', 'Regression: encode previously dropped', 'MB-driven port' and similar cross-reference comments left over from the malachite refactor. - MalachiteEvent: drop the inline Transactions payload from BlockProposal and BlockFinalized; BlockFinalized now carries height + block_hash alongside the certificate.
Replaces the inline 'find a finalized MB whose AdvanceTillEthereumBlock covers target' loops in ping_reorg_deeper_than_quarantine_breaks_mb_chain with a single helper. The helper accepts that the target may sit *inside* the MB's advanced eth-chain segment, not only as the AdvanceTillEthereumBlock target itself, so it works for any MB whose last_advanced_block walk covers the target.
…ance is not on canonical Eth chain
Both Coordinator and Participant now check that the last_advanced_block
of the locally-known latest finalized MB sits on the canonical Eth
chain ending at the current block. If a deep Eth reorg pushes that
advance off-chain:
- Coordinator: log::error and return Ok(None) — refusing to build
a batch. Future commitments are blocked until either the Eth
chain comes back to that branch or bad-block recovery logic is
implemented (TODO).
- Participant: rejects the validation request with the new
LatestFinalizedAdvanceNotCanonical reason so it does not co-sign
a batch the coordinator would not be able to land.
Implication is intentional: once a finalized MB has advanced to a
non-canonical Eth block we are stuck — the MB chain is immutable,
so we cannot rewrite the advance. A future bad-block compensation
path is needed to recover.
…_chain_head All chain-head bookkeeping is now driven from a single entry point. receive_new_chain_head still notifies the producer (chain_head_notify) and now also drains the pending-events queue that was previously released by the separate notify_block_synced call. Service-side BlockSynced handling collapses to one call.
…kData; ban unwrap_or for impossible cases PreparedBlockData carries last_committed_advanced_eth_block (zero at genesis), and setup_block_in_db stores it in BlockMeta so the 'prepared == true => last_committed_advanced_eth_block.is_some()' invariant holds without a None placeholder. try_include_checkpoint_chain_commitment no longer papers over the field with unwrap_or(zero); a None there now propagates as an error because the at_block is required to be prepared by that point. CLAUDE.md adds a project rule against using unwrap_or / unwrap_or_default / unwrap_or_else outside tests/mocks to mask invariants that should panic or surface as errors.
…ate end-of-test cleanup via stop_nodes - Replace every test-side log::info! with the atomic banner test_info! macro for consistent phase-boundary output across long integration tests. - Single-test cleanup tails go through stop_nodes([..]) instead of hand-rolled stop_service loops, so dropping is centralised and consistent. Mid-test stop+restart paths keep stop_service. - Drop the leftover '// === Restored tests (Task 2) ===' divider. - ping_reorg is #[ignore]d with a note: the canonical-advance check added in the previous commit blocks post-reorg commits until bad-block recovery lands.
…loop instead of take(3).collect() filter_map(...).take(3).collect() consumes the underlying Stream without going through InfiniteStreamExt::find_map, so the KickingStream's kick is never invoked. With manual Anvil mining the test would stall waiting for blocks that never arrive. Switch to a counted find_map loop that drives the kick on every iteration; the test now finishes in ~25s standalone (was hitting the 60s ntest timeout).
mailbox / send_injected_tx / uninitialized_program / value_send_delayed relied on the Drop path for tear-down; nextest's leak detector flags those runs as LEAK-FAIL. Adding an explicit stop_nodes(...) at the test tail closes the malachite WAL + libp2p listener cleanly.
ethexe-service integration tests spawn an Anvil child process, a malachite engine (libp2p + WAL + RocksDB), and a libp2p network service. Graceful tear-down of that whole stack at process exit can exceed nextest's default 5s leak-timeout, so the suite was failing with random LEAK-FAILs (mailbox / ping_deep_sync / multiple_validators_ping across runs) even though every test asserted ok. Override the leak policy for this package: 10s window with result = pass — leaks still surface in nextest output for inspection but do not fail the run. The proper fix is to make tear-down deterministic (await every spawned task, kill anvil explicitly), but the suite is currently dominated by external child-process timing that no test-level await can plug.
Drop the master flow entirely — the malachite branch already has its
own promise gossip (sign locally in service, broadcast full
SignedPromise via network) plus its own MB-driven compute pipeline.
Master's CompactPromise / SignedCompactPromise / PublishPromise event
chain assumes the announce-driven producer that malachite has
removed, so wiring it in would mean a second integration of the same
feature on top of the new flow.
Files reverted to malachite (HEAD): consensus/lib, compute/{compute,
lib,service,tests}, network/{gossipsub,lib,validator/topic},
processor/{lib,tests,handling/run/{mod,chunk_execution_spawn},
host/{mod,threads,api/promise}}, service/{lib,tests/{mod,utils/env,
utils/events}}, common/{db,injected,mock,primitives}, db/database,
core/src/rpc.
Files dropped: master's new ethexe/processor/src/promise.rs (BoundPromiseSink, Announce-keyed),
new ethexe/rpc/src/apis/injected/{mod,promise_manager,relay,server,
spawner,trait}.rs (master split injected.rs into a directory; the
malachite branch has heavily reworked the single injected.rs and the
new directory layout would need full reintegration). Also dropped:
master's bon dep in workspace Cargo.toml + compute Cargo.toml since
nothing uses bon::Builder after revert.
Files removed (already deleted on malachite side, master modified):
ethexe/consensus/src/connect/mod.rs, ethexe/consensus/src/validator/producer.rs.
Cargo.lock taken from master.
Adopt the new RpcMetricsLayer (per-method calls/latency tracking via jsonrpsee middleware). The new layer registers metrics for the methods listed in TRACKED_METHODS, so the inline counters in InjectedApi (`send_injected_tx_calls`, `send_and_watch_injected_tx_calls`, `injected_tx_promises_given`) become redundant — drop those calls from the malachite-side injected.rs and let the middleware handle them. The simplified `InjectedApiMetrics` (just `injected_tx_active_subscriptions`) is already what the malachite injected.rs uses; the inc/decrement calls survive unchanged. Master modified the now-deleted `injected/relay.rs` and `injected/server.rs` (the directory split from 4138374 that we did not adopt) — drop those changes. Pull in `scopeguard` workspace dep that the new RpcMetricsLayer relies on; 4138374 would have added it but we reverted that commit's RPC Cargo.toml changes.
Adopt the size-bounded Hashes response (master enforces MAX_RESPONSE_SIZE while accumulating CAS entries, so a single request for many large blobs no longer overflows the libp2p frame). The malachite branch already removed announce-driven db-sync (AnnouncesRequest, ProcessAnnounceError, the announce chain walk), so master's modifications to that surface — the imports of AnnouncesRequest/InnerAnnouncesResponse, the ProcessAnnounceError enum, the announce-only test cases, and the announces branch of response_from_db that called db.block_announces / db.announce_program_states — are dropped. Kept from master: the truncation logic, its `response_from_db_truncates_hashes_response_at_encoded_limit` test, and the new Compact / BTreeMap imports it needs. The malachite ProgramIds stub stays put (still a TODO until MB program states grow program-id query support).
…d and check workflows
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.